package org.apereo.cas.logging; import com.amazonaws.auth.AWSStaticCredentialsProvider; import com.amazonaws.auth.BasicAWSCredentials; import com.amazonaws.services.logs.AWSLogs; import com.amazonaws.services.logs.AWSLogsClient; import com.amazonaws.services.logs.AWSLogsClientBuilder; import com.amazonaws.services.logs.model.CreateLogGroupRequest; import com.amazonaws.services.logs.model.CreateLogStreamRequest; import com.amazonaws.services.logs.model.DataAlreadyAcceptedException; import com.amazonaws.services.logs.model.DescribeLogGroupsRequest; import com.amazonaws.services.logs.model.DescribeLogGroupsResult; import com.amazonaws.services.logs.model.DescribeLogStreamsRequest; import com.amazonaws.services.logs.model.DescribeLogStreamsResult; import com.amazonaws.services.logs.model.InputLogEvent; import com.amazonaws.services.logs.model.InvalidSequenceTokenException; import com.amazonaws.services.logs.model.LogStream; import com.amazonaws.services.logs.model.PutLogEventsRequest; import com.amazonaws.services.logs.model.PutLogEventsResult; import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.core.Layout; import org.apache.logging.log4j.core.LogEvent; import org.apache.logging.log4j.core.appender.AbstractAppender; import org.apache.logging.log4j.core.config.plugins.Plugin; import org.apache.logging.log4j.core.config.plugins.PluginAttribute; import org.apache.logging.log4j.core.config.plugins.PluginElement; import org.apache.logging.log4j.core.config.plugins.PluginFactory; import org.apache.logging.log4j.core.layout.PatternLayout; import java.io.Serializable; import java.util.ArrayList; import java.util.Comparator; import java.util.List; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; /** * This is {@link CloudWatchAppender}. * * @author Misagh Moayyed * @since 5.1.0 */ @Plugin(name = "CloudWatchAppender", category = "Core", elementType = "appender", printObject = true) public class CloudWatchAppender extends AbstractAppender { private static final long serialVersionUID = 1044758913028847477L; private static final int AWS_DRAIN_LIMIT = 256; private static final int AWS_LOG_STREAM_MAX_QUEUE_DEPTH = 10000; private static final int SHUTDOWN_TIMEOUT_MILLIS = 10000; private static final int AWS_LOG_STREAM_FLUSH_PERIOD_IN_SECONDS = 5; private final BlockingQueue<InputLogEvent> queue = new LinkedBlockingQueue<>(AWS_LOG_STREAM_MAX_QUEUE_DEPTH); private volatile boolean shutdown; private int flushPeriodMillis; private Thread deliveryThread; private final Object monitor = new Object(); /** * Every PutLogEvents request must include the sequenceToken obtained from the response of the previous request. */ private String sequenceTokenCache; private long lastReportedTimestamp = -1; private String logGroupName; private String logStreamName; private AWSLogs awsLogsClient; private volatile boolean queueFull; public CloudWatchAppender(final String name, final String awsLogGroupName, final String awsLogStreamName, final String awsLogStreamFlushPeriodInSeconds, final String credentialAccessKey, final String credentialSecretKey, final String awsLogRegionName, final Layout<Serializable> layout) { super(name, null, layout == null ? PatternLayout.createDefaultLayout() : layout, false); try { int flushPeriod = AWS_LOG_STREAM_FLUSH_PERIOD_IN_SECONDS; if (awsLogStreamFlushPeriodInSeconds != null) { flushPeriod = Integer.parseInt(awsLogStreamFlushPeriodInSeconds); } flushPeriodMillis = flushPeriod * 1_000; LOGGER.debug("Connecting to AWS CloudWatch..."); final AWSLogsClientBuilder builder = AWSLogsClient.builder(); final BasicAWSCredentials credentials = new BasicAWSCredentials(credentialAccessKey, credentialSecretKey); builder.setCredentials(new AWSStaticCredentialsProvider(credentials)); builder.setRegion(awsLogRegionName); this.awsLogsClient = builder.build(); this.logGroupName = awsLogGroupName; this.logStreamName = awsLogStreamName; this.sequenceTokenCache = createLogGroupAndLogStreamIfNeeded(); } catch (final Exception e) { LOGGER.error(e.getMessage(), e); } } private void flush() { int drained; final List<InputLogEvent> logEvents = new ArrayList<>(AWS_DRAIN_LIMIT); do { drained = queue.drainTo(logEvents, AWS_DRAIN_LIMIT); if (logEvents.isEmpty()) { break; } logEvents.sort(Comparator.comparing(InputLogEvent::getTimestamp)); if (lastReportedTimestamp > 0) { for (final InputLogEvent event : logEvents) { if (event.getTimestamp() < lastReportedTimestamp) { event.setTimestamp(lastReportedTimestamp); } } } lastReportedTimestamp = logEvents.get(logEvents.size() - 1).getTimestamp(); final PutLogEventsRequest putLogEventsRequest = new PutLogEventsRequest(logGroupName, logStreamName, logEvents); putLogEventsRequest.setSequenceToken(sequenceTokenCache); try { final PutLogEventsResult putLogEventsResult = awsLogsClient.putLogEvents(putLogEventsRequest); sequenceTokenCache = putLogEventsResult.getNextSequenceToken(); } catch (final DataAlreadyAcceptedException daae) { sequenceTokenCache = daae.getExpectedSequenceToken(); } catch (final InvalidSequenceTokenException iste) { sequenceTokenCache = iste.getExpectedSequenceToken(); } catch (final Exception e) { LOGGER.error(e.getMessage(), e); } logEvents.clear(); } while (drained >= AWS_DRAIN_LIMIT); } @Override public void append(final LogEvent logEvent) { final LogEvent event = LoggingUtils.prepareLogEvent(logEvent); final InputLogEvent awsLogEvent = new InputLogEvent(); final long timestamp = event.getTimeMillis(); final String message = new String(getLayout().toByteArray(event)); awsLogEvent.setTimestamp(timestamp); awsLogEvent.setMessage(message); if (!queue.offer(awsLogEvent) && !queueFull) { queueFull = true; } else if (queueFull) { queueFull = false; } } private String createLogGroupAndLogStreamIfNeeded() { LOGGER.debug("Attempting to locate the log group [{}]", logGroupName); final DescribeLogGroupsResult describeLogGroupsResult = awsLogsClient.describeLogGroups(new DescribeLogGroupsRequest().withLogGroupNamePrefix(logGroupName)); boolean createLogGroup = true; if (describeLogGroupsResult != null && describeLogGroupsResult.getLogGroups() != null && !describeLogGroupsResult.getLogGroups().isEmpty()) { createLogGroup = !describeLogGroupsResult.getLogGroups().stream().anyMatch(g -> g.getLogGroupName().equals(logGroupName)); } if (createLogGroup) { LOGGER.debug("Creating log group [{}]", logGroupName); final CreateLogGroupRequest createLogGroupRequest = new CreateLogGroupRequest(logGroupName); awsLogsClient.createLogGroup(createLogGroupRequest); } String logSequenceToken = null; boolean createLogStream = true; LOGGER.debug("Attempting to locate the log stream [{}] for group [{}]", logStreamName, logGroupName); final DescribeLogStreamsRequest describeLogStreamsRequest = new DescribeLogStreamsRequest(logGroupName).withLogStreamNamePrefix(logStreamName); final DescribeLogStreamsResult describeLogStreamsResult = awsLogsClient.describeLogStreams(describeLogStreamsRequest); if (describeLogStreamsResult != null && describeLogStreamsResult.getLogStreams() != null && !describeLogStreamsResult.getLogStreams().isEmpty()) { for (final LogStream ls : describeLogStreamsResult.getLogStreams()) { if (logStreamName.equals(ls.getLogStreamName())) { createLogStream = false; logSequenceToken = ls.getUploadSequenceToken(); LOGGER.debug("Found log stream [{}] with sequence token [{}]", logStreamName, logSequenceToken); break; } } } if (createLogStream) { LOGGER.debug("Creating log stream [{}] for group [{}]", logStreamName, logGroupName); final CreateLogStreamRequest createLogStreamRequest = new CreateLogStreamRequest(logGroupName, logStreamName); awsLogsClient.createLogStream(createLogStreamRequest); } return logSequenceToken; } @Override public void start() { super.start(); this.deliveryThread = new Thread(() -> { while (!shutdown) { try { flush(); } catch (final Throwable e) { LOGGER.error(e.getMessage(), e); } if (!shutdown && queue.size() < AWS_DRAIN_LIMIT) { try { synchronized (monitor) { monitor.wait(flushPeriodMillis); } } catch (final InterruptedException e) { LOGGER.error(e.getMessage(), e); } } } while (!queue.isEmpty()) { flush(); } }, "CloudWatchAppenderDeliveryThread"); deliveryThread.start(); } @Override public void stop() { super.stop(); shutdown = true; if (deliveryThread != null) { synchronized (monitor) { monitor.notify(); } try { deliveryThread.join(SHUTDOWN_TIMEOUT_MILLIS); } catch (final InterruptedException e) { LOGGER.error(e.getMessage(), e); } } if (queue.size() > 0) { flush(); } } /** * Create appender cloud watch appender. * * @param name the name * @param awsLogStreamName the aws log stream name * @param awsLogGroupName the aws log group name * @param awsLogStreamFlushPeriodInSeconds the aws log stream flush period in seconds * @param credentialAccessKey the credential access key * @param credentialSecretKey the credential secret key * @param awsLogRegionName the aws log region name * @param layout the layout * @return the cloud watch appender */ @PluginFactory public static CloudWatchAppender createAppender(@PluginAttribute("name") final String name, @PluginAttribute("awsLogStreamName") final String awsLogStreamName, @PluginAttribute("awsLogGroupName") final String awsLogGroupName, @PluginAttribute("awsLogStreamFlushPeriodInSeconds") final String awsLogStreamFlushPeriodInSeconds, @PluginAttribute("credentialAccessKey") final String credentialAccessKey, @PluginAttribute("credentialSecretKey") final String credentialSecretKey, @PluginAttribute("awsLogRegionName") final String awsLogRegionName, @PluginElement("Layout") final Layout<Serializable> layout) { return new CloudWatchAppender( name, awsLogGroupName, awsLogStreamName, awsLogStreamFlushPeriodInSeconds, StringUtils.defaultIfBlank(credentialAccessKey, System.getProperty("AWS_ACCESS_KEY")), StringUtils.defaultIfBlank(credentialSecretKey, System.getProperty("AWS_SECRET_KEY")), StringUtils.defaultIfBlank(awsLogRegionName, System.getProperty("AWS_REGION_NAME")), layout); } }